#ifndef OPENGL_DEVICE_H
#define OPENGL_DEVICE_H

#include <iostream>

#include "opengl_inc.h"
#include "output_device.h"
#include "picking_device.h"
#include "camera_control_device.h"
#include "vector4.h"
#include "vector3.h"
#include "matrix4.h"
#include "scene.h"
#include "sceneelement.h"
#include "camera.h"
#include "oglchecker.h"
#include "primitivebuffer.h"
#include "point_buffer.h"
#include "output_device_visitor.h"

class OpenGLDevice : public CameraControlDevice, public PickingDevice, public OutputDevice
{
public:
  OpenGLDevice(Controller& c) :
    CameraControlDevice(c), PickingDevice(c),
    scene(c.scene()), max_primitives(200), numSamples(8),
    addingSelection(false), initialized(false), modified(false) {}
  virtual void init() 
  {
    GLenum err = glewInit();
    if (GLEW_OK != err)
    {
      /* Problem: glewInit failed, something is seriously wrong. */
      std::cerr << "Error: " << glewGetErrorString(err) << std::endl;
    }
    if (!glewIsSupported("GL_VERSION_3_3"))
    {
      std::cerr << "Error: OpenGL 3.3 not supported." << std::endl;
    }

    glEnable(GL_DEPTH_TEST);
    bgcolor = scene.backgroundColor();
    glClearColor(bgcolor.ambient()[0], bgcolor.ambient()[1], bgcolor.ambient()[2], bgcolor.ambient()[3]);
    
    glGetIntegerv(GL_MAX_SAMPLES, &numSamples);

    createFBO();
    compilePrimitives();
    compilePrimitiveBuffers();
    compileShaders();
    initialized = true;
  }
  ~OpenGLDevice()
  {
    ellipsoidBuffer.deInit();
    coneBuffer.deInit();
    parallelepipedBuffer.deInit();
    cylinderBuffer.deInit();
    ellipsoidSelBuffer.deInit();
    coneSelBuffer.deInit();
    parallelepipedSelBuffer.deInit();
    cylinderSelBuffer.deInit();
    sphere.deInit();
    cube.deInit();
    cone.deInit();
    cylinder.deInit();

    if (initialized)
    {
      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
      glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
      glDeleteRenderbuffers(4, rbo);
      glDeleteFramebuffers(2, fbo);
    }
  }
  virtual void resize(unsigned int w, unsigned int h)
  {
    resolution[0] = w;
    resolution[1] = h;
    glViewport(0, 0, w, h);
    resizeFBO();
    modified = true;
  }

  virtual void clear()
  {
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[1]);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[0]);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
  }
  virtual void setScene(Scene& s ) { scene = s; }

  virtual void updateCamera()
  {
    modified = true;
  }

  virtual void redraw()
  {
    if (modified)
    {
      refresh();
      modified = false;
    }
    renderToBackBuffer();
  }

  virtual void updateSelection()
  {
    ellipsoidSelBuffer.clear();
    coneSelBuffer.clear();
    parallelepipedSelBuffer.clear();
    cylinderSelBuffer.clear();
    pointSelBuffer.clear();
    lineSelBuffer.clear();
    addingSelection = true;
    OutputDeviceVisitor visitor(*this);
    for (Scene::element_const_iterator i = scene.selectionBegin(), ie = scene.selectionEnd(); i != ie; ++i) (*i)->visit(visitor);

    ellipsoidSelBuffer.uploadData();
    coneSelBuffer.uploadData();
    parallelepipedSelBuffer.uploadData();
    cylinderSelBuffer.uploadData();
    pointSelBuffer.uploadData();
    lineSelBuffer.uploadData();

    modified = true;
  }
  virtual void updateAll() 
  {
    clearBuffers();

    OutputDeviceVisitor visitor(*this);
    addingSelection = false;
    for (Scene::element_const_iterator i = scene.elementsBegin(), ie = scene.elementsEnd(); i != ie; ++i) (*i)->visit(visitor);
    addingSelection = true;
    for (Scene::element_const_iterator i = scene.selectionBegin(), ie = scene.selectionEnd(); i != ie; ++i) (*i)->visit(visitor);

    ellipsoidBuffer.uploadData();
    coneBuffer.uploadData();
    parallelepipedBuffer.uploadData();
    cylinderBuffer.uploadData();
    pointBuffer.uploadData();
    lineBuffer.uploadData();
    ellipsoidSelBuffer.uploadData();
    coneSelBuffer.uploadData();
    parallelepipedSelBuffer.uploadData();
    cylinderSelBuffer.uploadData();
    pointSelBuffer.uploadData();
    lineSelBuffer.uploadData();

    modified = true;
  }
  
  virtual void drawSphere(const Sphere& o) 
  {
    if (addingSelection) ellipsoidSelBuffer.addPrimitive(o.material(), o.matrix(), o.id());
      else ellipsoidBuffer.addPrimitive(o.material(), o.matrix(), o.id());
  }
  virtual void drawEllipsoid(const Ellipsoid& o) 
  {
    if (addingSelection) ellipsoidSelBuffer.addPrimitive(o.material(), o.matrix(), o.id());
      else ellipsoidBuffer.addPrimitive(o.material(), o.matrix(), o.id());
  }
  virtual void drawCylinder(const Cylinder& o)  
  {
    if (addingSelection) cylinderSelBuffer.addPrimitive(o.material(), o.matrix(), o.id());
      else cylinderBuffer.addPrimitive(o.material(), o.matrix(), o.id());
  }
  virtual void drawCone(const Cone& o) 
  {
    if (addingSelection) coneSelBuffer.addPrimitive(o.material(), o.matrix(), o.id());
      else coneBuffer.addPrimitive(o.material(), o.matrix(), o.id());
  }
  virtual void drawParalelepiped(const Paralelepiped& o) 
  {
    if (addingSelection) parallelepipedSelBuffer.addPrimitive(o.material(), o.matrix(), o.id());
      else parallelepipedBuffer.addPrimitive(o.material(), o.matrix(), o.id());
  }
  virtual void drawSurface(const Surface& o) {}
  virtual void drawPoint(const Point& o)
  {
    if (addingSelection) pointSelBuffer.addPrimitive(o.material(), o.matrix(), o.id());
      else pointBuffer.addPoint(o.coord(), Vector4f(o.material().ambient()), o.id());
  }
  virtual void drawLine(const Line& o)
  {
    if (addingSelection) lineSelBuffer.addPrimitive(o.material(), o.matrix(), o.id());
      else lineBuffer.addPoint(o.coord(), Vector4f(o.material().ambient()), o.id());
  }
  virtual void drawText(const Text& o) {}

  virtual IdGenerator::id_type elementAt(const screen_pos_type& pos) 
  {

    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[1]);
    glClearColor(0.0, 0.0, 0.0, 0.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    glClearColor(bgcolor.ambient()[0], bgcolor.ambient()[1], bgcolor.ambient()[2], bgcolor.ambient()[3]);
    
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    shaderManager.selectShader(OGLShaderManager::SELECTION);
    Matrix4f vpMatrix = scene.camera().projection(float(resolution.x())/float(resolution.y()))*scene.camera().view();
    glUniformMatrix4fv(shaderManager.uniformLocation(OGLShaderManager::SELECTION, "vpMatrix"),
                                      1, GL_FALSE, vpMatrix.data());
    ellipsoidBuffer.drawSelection();
    ellipsoidSelBuffer.drawSelection();
    coneBuffer.drawSelection();
    coneSelBuffer.drawSelection();
    parallelepipedBuffer.drawSelection();
    parallelepipedSelBuffer.drawSelection();
    cylinderBuffer.drawSelection();
    cylinderSelBuffer.drawSelection();
    pointSelBuffer.drawSelection();
    lineSelBuffer.drawSelection();

    shaderManager.selectShader(OGLShaderManager::SELECTION);
    glUniformMatrix4fv(shaderManager.uniformLocation(OGLShaderManager::SELECTION, "vpMatrix"),
                                      1, GL_FALSE, vpMatrix.data());
    lineBuffer.drawSelection();
    pointBuffer.drawSelection();

    GLubyte result[4];
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
    glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo[1]);
    glReadBuffer(GL_COLOR_ATTACHMENT0);
    glReadPixels(pos.x(), resolution.y() - pos.y(), 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, result);
    unsigned int id = ((unsigned int)(result[0])<<24) + ((unsigned int)(result[1])<<16)+((unsigned int)(result[2])<<8)+((unsigned int)(result[3]));
    return id;
  }
private:
  void clearBuffers()
  {
    ellipsoidBuffer.clear();
    coneBuffer.clear();
    parallelepipedBuffer.clear();
    cylinderBuffer.clear();
    pointBuffer.clear();
    lineBuffer.clear();
    ellipsoidSelBuffer.clear();
    coneSelBuffer.clear();
    parallelepipedSelBuffer.clear();
    cylinderSelBuffer.clear();
    pointSelBuffer.clear();
    lineSelBuffer.clear();
  }
  void createFBO()
  {
    glGenFramebuffers(2, fbo); 
    glGenRenderbuffers(4, rbo);

    // stub with resolution to allocate some space for renderbuffers
    resolution[0] = 800;
    resolution[1] = 600;
    resizeFBO();

    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[1]);   
    glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo[3]);
    glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo[2]);
    OGLChecker::checkFBO(GL_DRAW_FRAMEBUFFER);

    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[0]);
    glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo[1]);
    glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo[0]);
    OGLChecker::checkFBO(GL_DRAW_FRAMEBUFFER);
  }
  void resizeFBO()
  {
    glBindRenderbuffer(GL_RENDERBUFFER, rbo[0]);
    glRenderbufferStorageMultisample(GL_RENDERBUFFER, numSamples, GL_RGBA8, resolution[0], resolution[1]);
    glBindRenderbuffer(GL_RENDERBUFFER, rbo[1]);
    glRenderbufferStorageMultisample(GL_RENDERBUFFER, numSamples, GL_DEPTH24_STENCIL8, resolution[0], resolution[1]);
    glBindRenderbuffer(GL_RENDERBUFFER, rbo[2]);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, resolution[0], resolution[1]);
    glBindRenderbuffer(GL_RENDERBUFFER, rbo[3]);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, resolution[0], resolution[1]);
  }
  void compilePrimitives()
  {
    sphere.init();
    cube.init();
    cone.init();
    cylinder.init();
    sphere.fillSphere();
    sphere.uploadPrimitive();
    cube.fillCube();
    cube.uploadPrimitive();
    cone.fillCone();
    cone.uploadPrimitive();
    cylinder.fillCylinder();
    cylinder.uploadPrimitive();
  }
  void compilePrimitiveBuffers()
  {
    ellipsoidBuffer.setPrimitive(&sphere);
    ellipsoidSelBuffer.setPrimitive(&sphere);
    coneBuffer.setPrimitive(&cone);
    coneSelBuffer.setPrimitive(&cone);
    parallelepipedBuffer.setPrimitive(&cube);
    parallelepipedSelBuffer.setPrimitive(&cube);
    cylinderBuffer.setPrimitive(&cylinder);
    cylinderSelBuffer.setPrimitive(&cylinder);
    pointSelBuffer.setPrimitive(&sphere);
    lineSelBuffer.setPrimitive(&cylinder);
    ellipsoidBuffer.init();
    ellipsoidSelBuffer.init();
    coneBuffer.init();
    coneSelBuffer.init();
    parallelepipedBuffer.init();
    parallelepipedSelBuffer.init();
    cylinderBuffer.init();
    cylinderSelBuffer.init();
    pointBuffer.init();
    lineBuffer.init();
  }
  void compileShaders()
  {
    shaderManager.compileShaders();
  }
  void refresh()
  {
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[0]);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    shaderManager.selectShader(OGLShaderManager::PHONG);
    Vector3 lightPos = scene.camera().lightPos();
    Vector3 camPos = scene.camera().pos();
    Matrix4f vpMatrix = scene.camera().projection(float(resolution.x())/float(resolution.y()))*scene.camera().view();
    const Material& lightColor = scene.camera().lightColor();
    glUniform4f(shaderManager.uniformLocation(OGLShaderManager::PHONG, "lightPos"),
                                      lightPos.x(), lightPos.y(), lightPos.z(), 1.0);
    glUniform4f(shaderManager.uniformLocation(OGLShaderManager::PHONG, "cameraPos"),
                                      camPos.x(), camPos.y(), camPos.z(), 1.0);
    glUniform4fv(shaderManager.uniformLocation(OGLShaderManager::PHONG, "lightAmbient"),
                                      1, lightColor.ambient());
    glUniform4fv(shaderManager.uniformLocation(OGLShaderManager::PHONG, "lightDiffuse"),
                                      1, lightColor.diffuse());
    glUniform4fv(shaderManager.uniformLocation(OGLShaderManager::PHONG, "lightSpecular"),
                                      1, lightColor.specular());
    glUniformMatrix4fv(shaderManager.uniformLocation(OGLShaderManager::PHONG, "vpMatrix"),
                                      1, GL_FALSE, vpMatrix.data());
    ellipsoidBuffer.draw();
    coneBuffer.draw();
    parallelepipedBuffer.draw();
    cylinderBuffer.draw();

    shaderManager.selectShader(OGLShaderManager::WIREFRAME);
    glUniformMatrix4fv(shaderManager.uniformLocation(OGLShaderManager::WIREFRAME, "vpMatrix"),
                                      1, GL_FALSE, vpMatrix.data());
    lineBuffer.draw();
    pointBuffer.draw();

    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    shaderManager.selectShader(OGLShaderManager::FLAT);
    Vector4f selectionColor(0.0, 1.0, 0.0, 1.0);
    glUniform4fv(shaderManager.uniformLocation(OGLShaderManager::FLAT, "color"), 1, selectionColor.vertex());
    glUniformMatrix4fv(shaderManager.uniformLocation(OGLShaderManager::FLAT, "vpMatrix"),
                                      1, GL_FALSE, vpMatrix.data());
    ellipsoidSelBuffer.drawFlat();
    coneSelBuffer.drawFlat();
    parallelepipedSelBuffer.drawFlat();
    cylinderSelBuffer.drawFlat();
    pointSelBuffer.drawFlat();
    lineSelBuffer.drawFlat();
  }
  void renderToBackBuffer()
  {   
    glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo[0]);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
    glBlitFramebuffer(0, 0, resolution.x(), resolution.y(),
                      0, 0, resolution.x(), resolution.y(),
                      GL_COLOR_BUFFER_BIT, GL_NEAREST);

    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[0]);
  }

  Scene& scene;
  GLuint fbo[2]; // for drawing and selection
  GLuint rbo[4]; // for color and depth data
  
  OGLPrimitiveBuffer<VertexNoData> ellipsoidBuffer, parallelepipedBuffer, coneBuffer, cylinderBuffer;
  OGLPrimitiveBuffer<VertexNoData> ellipsoidSelBuffer, parallelepipedSelBuffer, coneSelBuffer, cylinderSelBuffer, lineSelBuffer, pointSelBuffer;
  OGLPointBuffer pointBuffer, lineBuffer;
  OGLPrimitive<VertexNoData> sphere, cube, cone, cylinder;
  OGLShaderManager shaderManager;
  int max_primitives, numSamples;
  Vector3T<int> resolution;
  bool addingSelection;
  bool initialized;
  Material bgcolor;
  bool modified;
};

#endif // OPENGL_DEVICE_H
